package com.fly.git.service;

import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.eclipse.jgit.api.AddCommand;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.CommitCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.PushCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.InvalidRemoteException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import com.fly.core.exception.ValidateException;
import com.fly.core.utils.HttpRequestUtils;
import com.fly.git.model.GitInfo;
import com.fly.git.model.GitRunInfo;

import lombok.extern.slf4j.Slf4j;

/**
 * 
 * GitClientService
 * 
 * @author 00fly
 * @version [版本号, 2021年6月27日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Slf4j
@Service
public class GitClientService
{
    private List<GitRunInfo> gitRunInfos = new ArrayList<>();
    
    public List<GitRunInfo> getGitRunInfos()
    {
        return gitRunInfos;
    }
    
    /**
     * 初始化
     * 
     * @see [类、类#方法、类#成员]
     */
    @PostConstruct
    public void init()
    {
        // 遍历data目录
        File data = new File("data");
        if (!data.exists())
        {
            data.mkdirs();
            log.error("####### data directory is empty!");
            return;
        }
        Collection<File> files = FileUtils.listFiles(data, new String[] {"ser"}, false);
        if (files.isEmpty())
        {
            log.error("####### not find any file in data directory!");
            return;
        }
        gitRunInfos.clear();
        for (File file : files)
        {
            try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)))
            {
                GitInfo gitInfo = (GitInfo)ois.readObject();
                Assert.notNull(gitInfo, "gitInfo is null");
                Assert.isTrue(StringUtils.isNoneEmpty(gitInfo.getUsername(), gitInfo.getPassword(), gitInfo.getEmail(), gitInfo.getRemoteGit()), "git params is empty");
                GitRunInfo gitRunInfo = new GitRunInfo(gitInfo);
                gitRunInfos.add(gitRunInfo);
                log.info("****** init from file [{}] success!", file.getAbsolutePath());
            }
            catch (IOException | ClassNotFoundException e)
            {
                log.error("###### init failure! {}", e.getMessage());
            }
        }
    }
    
    /**
     * 克隆远程库
     * 
     * @throws InvalidRemoteException
     * @throws TransportException
     * @throws GitAPIException
     * @see [类、类#方法、类#成员]
     */
    public void gitClone(GitRunInfo gitRunInfo)
        throws InvalidRemoteException, TransportException, GitAPIException
    {
        GitInfo gitInfo = gitRunInfo.getGitInfo();
        Assert.notNull(gitInfo, "gitInfo is null");
        CloneCommand cloneCommand = Git.cloneRepository();
        cloneCommand.setURI(gitInfo.getRemoteGit()).setBranch("master").setCredentialsProvider(gitRunInfo.getCredentialsProvider());
        cloneCommand.setDirectory(new File(gitRunInfo.getLocalPath()));
        try (Git git = cloneCommand.call())
        {
            log.info("★★★★ gitClone success!  tag: {}", git.tag());
        }
    }
    
    /**
     * 本地提交代码
     */
    public void localCommit(GitRunInfo gitRunInfo)
        throws IOException, GitAPIException, JGitInternalException
    {
        GitInfo gitInfo = gitRunInfo.getGitInfo();
        Assert.notNull(gitInfo, "gitInfo is null");
        try (Git git = new Git(new FileRepository(gitRunInfo.getLocalPath() + "/.git")))
        {
            String remark = DateFormatUtils.format(System.currentTimeMillis(), "yyyy-MM-dd HH:mm:ss") + " auto commit.";
            git.commit().setAmend(true).setAuthor(gitInfo.getUsername(), gitInfo.getEmail()).setMessage(remark).call();
        }
    }
    
    /**
     * 拉取远程仓库内容到本地
     */
    public void remotePull(GitRunInfo gitRunInfo)
        throws IOException, GitAPIException
    {
        GitInfo gitInfo = gitRunInfo.getGitInfo();
        Assert.notNull(gitInfo, "gitInfo is null");
        try (Git git = new Git(new FileRepository(gitRunInfo.getLocalPath() + "/.git")))
        {
            git.pull().setRemote("origin").setRemoteBranchName("master").setCredentialsProvider(gitRunInfo.getCredentialsProvider()).call();
        }
    }
    
    /**
     * push本地代码到远程仓库地址
     */
    public void remotePush(GitRunInfo gitRunInfo)
        throws IOException, JGitInternalException, GitAPIException
    {
        GitInfo gitInfo = gitRunInfo.getGitInfo();
        Assert.notNull(gitInfo, "gitInfo is null");
        try (Git git = new Git(new FileRepository(gitRunInfo.getLocalPath() + "/.git")))
        {
            git.push().setRemote("origin").setCredentialsProvider(gitRunInfo.getCredentialsProvider()).call();
        }
    }
    
    /**
     * 定时任务调度逻辑
     * 
     * @see [类、类#方法、类#成员]
     */
    @Scheduled(cron = "0 0 0/1 * * ?")
    public void schedule()
    {
        for (GitRunInfo gitRunInfo : gitRunInfos)
        {
            int curHour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
            Set<Integer> runHours = gitRunInfo.getRunHours();
            if (runHours.isEmpty() || curHour == 0)
            {
                gitRunInfo.registerHours();
            }
            log.info("---- runHours is {} ----", runHours);
            
            // 到达设定时间点或当天未提交过
            String date = DateFormatUtils.format(System.currentTimeMillis(), "yyyy-MM-dd");
            String commitText = gitRunInfo.getCommitText();
            if (runHours.contains(curHour) || !StringUtils.startsWith(commitText, date))
            {
                try
                {
                    // 删除project目录
                    if (SystemUtils.IS_OS_UNIX)
                    {
                        Runtime.getRuntime().exec(new String[] {"sh", "-c", "rm -rf " + gitRunInfo.getLocalPath()});
                    }
                    else if (SystemUtils.IS_OS_WINDOWS)
                    {
                        Runtime.getRuntime().exec("cmd /c start " + gitRunInfo.getLocalPath());
                        TimeUnit.SECONDS.sleep(1);
                        Runtime.getRuntime().exec("cmd /c rd /s/q " + gitRunInfo.getLocalPath());
                    }
                    long seconds = RandomUtils.nextLong(10, 100L);
                    log.info("★★★★ thread will sleep seconds： {}", seconds);
                    TimeUnit.SECONDS.sleep(seconds);
                    runGitAll();
                }
                catch (InterruptedException | IOException | GitAPIException e)
                {
                    log.error("schedule error", e.getCause());
                }
            }
        }
    }
    
    /**
     * 全流程
     * 
     * @throws IOException
     * @throws GitAPIException
     * @throws InterruptedException
     * 
     * @see [类、类#方法、类#成员]
     */
    public void runGitAll()
        throws IOException, GitAPIException
    {
        for (GitRunInfo gitRunInfo : gitRunInfos)
        {
            GitInfo gitInfo = gitRunInfo.getGitInfo();
            Assert.notNull(gitInfo, "gitInfo is null");
            
            // 仅限制http接口请求，不影响定时任务
            if (HttpRequestUtils.getHttpServletRequest() != null)
            {
                Long lastRunTime = gitRunInfo.getLastRunTime();
                if (lastRunTime != null && System.currentTimeMillis() < lastRunTime + 600000)
                {
                    String msg = String.format("您请求过于频繁，请%s后再试", DateFormatUtils.format(lastRunTime + 600000, "HH:mm:ss"));
                    throw new ValidateException(msg);
                }
                gitRunInfo.setLastRunTime(System.currentTimeMillis());
            }
            log.info("★★★★ it is time to ready run ......");
            if (!new File(gitRunInfo.getLocalPath()).exists())
            {
                gitClone(gitRunInfo);
            }
            
            // 替换最后一行内容
            File readMe = new File(gitRunInfo.getLocalPath() + "/README.md");
            List<String> lines = FileUtils.readLines(readMe, StandardCharsets.UTF_8);
            if (lines.size() > 1)
            {
                lines.remove(lines.size() - 1);
            }
            String commitText = DateFormatUtils.format(System.currentTimeMillis(), "yyyy-MM-dd HH:mm:ss");
            lines.add(commitText);
            FileUtils.writeLines(readMe, StandardCharsets.UTF_8.name(), lines);
            gitRunInfo.setCommitText(commitText);
            
            File rootDir = new File(gitRunInfo.getLocalPath());
            try (Git git = Git.open(rootDir))
            {
                List<String> files = Arrays.asList(rootDir.list(new FilenameFilter()
                {
                    @Override
                    public boolean accept(File dir, String name)
                    {
                        return !name.endsWith(".git");
                    }
                }));
                List<DiffEntry> diffEntries = git.diff().setPathFilter(PathFilterGroup.createFromStrings(files)).setShowNameAndStatusOnly(true).call();
                if (diffEntries == null || diffEntries.isEmpty())
                {
                    return;
                }
                // 被修改过的文件
                List<String> updateFiles = new ArrayList<String>();
                ChangeType changeType;
                for (DiffEntry entry : diffEntries)
                {
                    changeType = entry.getChangeType();
                    switch (changeType)
                    {
                        case ADD:
                        case COPY:
                        case RENAME:
                        case MODIFY:
                            updateFiles.add(entry.getNewPath());
                            break;
                        case DELETE:
                            updateFiles.add(entry.getOldPath());
                            break;
                    }
                }
                
                // 将文件提交到git仓库中，并返回本次提交的版本号
                // 1、将工作区的内容更新到暂存区
                AddCommand addCmd = git.add();
                for (String file : updateFiles)
                {
                    addCmd.addFilepattern(file);
                }
                addCmd.call();
                
                // 2、commit
                CommitCommand commitCmd = git.commit();
                for (String file : updateFiles)
                {
                    commitCmd.setOnly(file);
                }
                RevCommit revCommit = commitCmd.setCommitter(gitInfo.getUsername(), gitInfo.getEmail()).setMessage(commitText + " auto commit.").call();
                log.info("★★★★ local commit successful:{}", revCommit.getName());
                
                // swagger测试忽略远程提交
                if (HttpRequestUtils.getHttpServletRequest() != null)
                {
                    log.info("★★★★ Swagger HttpServletRequest Test, not call remote git push ★★★★ ");
                    continue;
                }
                
                // 3、git push
                log.info("★★★★ now call remote git push ★★★★");
                PushCommand push = git.push();
                push.setRemote("origin").setCredentialsProvider(gitRunInfo.getCredentialsProvider()).call();
                log.info("★★★★ remote git push successful ★★★★\n");
            }
        }
    }
    
    /**
     * autoCommitPull
     * 
     * @return
     * @see [类、类#方法、类#成员]
     */
    // @Scheduled(cron = "0 0 9-17 * * ?")
    public void autoCommitPullAll()
    {
        for (GitRunInfo gitRunInfo : gitRunInfos)
        {
            autoCommitPull(gitRunInfo);
        }
    }
    
    private boolean autoCommitPull(GitRunInfo gitRunInfo)
    {
        try
        {
            log.info("★★★★ autoCommitPull ★★★★");
            if (!new File(gitRunInfo.getLocalPath()).exists())
            {
                gitClone(gitRunInfo);
            }
            localCommit(gitRunInfo);
            remotePush(gitRunInfo);
            return true;
        }
        catch (JGitInternalException | GitAPIException | IOException e)
        {
            log.error("autoCommitPull error", e.getCause());
            return false;
        }
    }
}
